//----------------------------------------------------------------------------
//  ,-.
// /_\___ ,   Ninja Framework
// '  ___\    (C) Riccardo Capra - http://riccar.do
//  //   \\
//----------------------------------------------------------------------------
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//----------------------------------------------------------------------------

#ifndef NJ_RW_LOCK_H
#	define NJ_RW_LOCK_H

#	include "NjNinjaSyncLib.h"
#	include "NjLocker.h"
#	include "NjEventsSystem.h"
#	include "NjRWLock_config.h"
#	include "NjConfiguration.h"

	/// \brief Implements a read-write lock synchronization object
	///
	/// This kind of synchronization primitives allows multiple concurrent read locks made by different threads and exclusive write lock.
	/// Similarly to mutexes, the thread that locks for read/write is also responsible to call the corresponding UnlockRead/UnlockWrite.
	/// LockWrite is not recursive. This means that once a thread has called LockWrite it cannot call LockRead, TryLockRead, UnlockRead, LockWrite or TryLockWrite.
	/// The only allowed operation is UnlockWrite. Calling one of the other methods, the behaviour is undefined and can cause deadlocks.
	/// LockRead is recursive in relation to read operations only. This means that once a thread has called LockRead it cannot call LockWrite, TryLockWrite or UnlockWrite.
	/// The only allowed operations are LockRead, TryLockRead or UnlockRead. Calling one of the other methods, the behaviour is undefined and can cause deadlocks.
	/// A thread should pair each LockRead with a UnlockRead before calling LockWrite.
	class NjRWLock
	{
	public:
		/// \brief Defines the timeout type
		typedef NjEventsSystem::TimeMs TimeMs;

		/// \brief This is class adapts the read lock/unlock methods to the pattern required by NjLocker
		class Read
		{
		public:
			/// \brief Defines a scoped-lock for Read locks on NjRWLock
			typedef NjLocker<Read> Locker;

			/// \copydoc NjRWLock::LockRead
			void Lock();

			/// \copydoc NjRWLock::TryLockRead
			NjBool TryLock(TimeMs uTimeMs);

			/// \copydoc NjRWLock::UnlockRead
			void Unlock();

			/// \brief Returns a reference to the NjRWLock instance related to this Read instance
			NjRWLock & GetRWLock();

		private:
			NJ_DISABLE_INSTANCE(Read);
		};

		/// \brief This is class adapts the write lock/unlock methods to the pattern required by NjLocker
		class Write
		{
		public:
			/// \brief Defines a scoped-lock for Write locks on NjRWLock
			typedef NjLocker<Write> Locker;

			/// \copydoc NjRWLock::LockWrite
			void Lock();

			/// \copydoc NjRWLock::TryLockWrite
			NjBool TryLock(TimeMs uTimeMs);

			/// \copydoc NjRWLock::UnlockWrite
			void Unlock();

			/// \brief Returns a reference to the NjRWLock instance related to this Write instance
			NjRWLock & GetRWLock();

		private:
			NJ_DISABLE_INSTANCE(Write);
		};

		/// \copydoc Read::Locker
		typedef Read::Locker LockerRead;

		/// \copydoc Write::Locker
		typedef Write::Locker LockerWrite;

		/// \brief Creates a NjRWLock instance
		NjRWLock();

		/// \brief Destroys this NjRWLock instance
		~NjRWLock();

		/// \brief Scquires the read-lock
		///
		/// At any time read locks are exclusive in relation to write locks.
		/// Threads attempting a LockWrite will be suspended until the lock is released by an UnlockRead call, vice-versa if the lock is already write-locked,
		/// the thread calling LockRead will be suspended until UnlockWrite is called
		void LockRead();

		/// \brief Attempts to acquire the read-lock
		///
		/// TryLockRead(0) will attempt to acquire the lock and will return immediately if the lock is already write-locked, without waiting further.
		/// TryLockRead(NjEventsSystem::WAIT_FOREVER) is identical to LockRead
		/// \param uTimeMs The time out in milliseconds
		/// \return If the lock is acquired within the provided time out, TryLockRead will return NJ_TRUE otherwise it will return NJ_FALSE
		NjBool TryLockRead(TimeMs uTimeMs);

		/// \brief Releases a previously acquired read-lock
		///
		/// Just the thread that initially successfully acquired the read-lock can call UnlockRead
		void UnlockRead();

		/// \brief Acquires the write-lock
		///
		/// The write-lock is exclusive. Once acquired, no other threads will be able to acquire it for read or write.
		/// The other threads will be suspended until the write-lock is released by an UnlockWrite call
		void LockWrite();

		/// \brief Attempts to acquire the write-lock
		///
		/// TryLockWrite(0) will attempt to acquire the write-lock and will return immediately if the is already locked by read or write, without waiting further. 
		/// TryLockWrite(NjEventsSystem::WAIT_FOREVER) is identical to LockWrite
		/// \param uTimeMs The time out in milliseconds
		/// \return If the lock is acquired within the provided time out, TryLockWrite will return NJ_TRUE otherwise it will return NJ_FALSE
		NjBool TryLockWrite(TimeMs uTimeMs);

		/// \brief Releases a previously acquired write-lock
		///
		/// Just the thread that initially successfully acquired the write-lock can call UnlockWrite
		/// This condition is enforced in some build configurations through assertions
		void UnlockWrite();

		/// \brief Returns a reference to a NjRWLock::Read type operating on this instance of NjRWLock
		///
		/// The reference to NjRWLock::Read does not require any memory allocation. So there is no data to be allocated or freed
		Read & GetRead();

		/// \brief Returns a reference to a NjRWLock::Write type operating on this instance of NjRWLock
		///
		/// The reference to NjRWLock::Write does not require any memory allocation. So there is no data to be allocated or freed
		Write & GetWrite();

	private:
		/// \brief The kernel event type
		typedef NjEventsSystem::Event Event;

		/// \brief Internally called to unlock for read or write
		/// \param uState The last read state for \em m_kState
		/// \param uEventUnsetBit The unset bit to update (either STATE_READ_EVENT_UNSET or STATE_WRITE_EVENT_UNSET)
		/// \param kEvent The involved kernel event (either \em m_kReadEvent or \em m_kWriteEvent )
		NjUint UnlockInternal(NjUint uState, NjUint uEventUnsetBit, NjAtomic<Event *> & kEvent);

		/// \brief Internally unlocks for write
		/// \param bUnlockFromLock set to NJ_FALSE if called by UnlockWrite and NJ_TRUE if called by TryLockWrite to clean up the NjRWLock state after it has timed out
		void UnlockWriteInternal(NjBool bUnlockFromLock);

		/// \brief Used to store the state of the kernel event object \em m_kReadEvent
		///
		/// This bit can be set only if the kernel object is allocated.
		/// When this bit is set it is expected that NjEventsSystem::TryWaitEvent operations will block the calling thread
		static const NjUint STATE_READ_EVENT_UNSET  = 0x00000001;

		/// \brief Used to store the state of the kernel event object \em m_kWriteEvent
		///
		/// This bit can be set only if the kernel object is allocated.
		/// When this bit is set it is expected that NjEventsSystem::TryWaitEvent operations will block the calling thread
		static const NjUint STATE_WRITE_EVENT_UNSET = 0x00000002;

		/// \brief The bit used to mark if the NjRWLock is locked for write or not
		static const NjUint STATE_WRITE_LOCKED      = 0x00000004;

		/// \brief Counter step used by the NjRWLock implementation to account how many times has been locked for read
		static const NjUint STATE_READ_COUNTER_ONE  = 0x00000008;

		/// \var static const NjUint STATE_READ_COUNTER_MASK
		/// \brief Counter mask used by the NjRWLock implementation to account how many times has been locked for read

#if NJ_PLATFORM_64_BIT
		static const NjUint STATE_READ_COUNTER_MASK = 0x00000003FFFFFFF8ULL;
#else // NJ_PLATFORM_64_BIT
		static const NjUint STATE_READ_COUNTER_MASK = 0x0003FFF8;
#endif // NJ_PLATFORM_64_BIT

		/// \brief A tag counter used to avoid the ABA problem
		/// \sa http://en.wikipedia.org/wiki/ABA_problem
		static const NjUint STATE_ABA_COUNTER_ONE   = STATE_READ_COUNTER_MASK+STATE_READ_COUNTER_ONE;

#	if NJ_RW_LOCK_TRACKING_ENABLED
		/// \brief Class to track rw locks usage
		class TrackerEntry;

		/// \copydoc TrackerEntry
		class Tracker;
#	endif // NJ_RW_LOCK_TRACKING_ENABLED

		/// \brief The current state of the rw lock
		NjAtomic<NjUint> m_kState;

		/// \brief The kernel event assigned to this rw lock for read
		NjAtomic<Event *> m_kReadEvent;

		/// \brief The kernel event assigned to this rw lock for write
		NjAtomic<Event *> m_kWriteEvent;

#	if NJ_RW_LOCK_STORE_WRITE_LOCKER_ENABLED
		/// \brief Stores the current thread locking the resource for write
		NjAtomic<NjUint> m_kThreadDebugIdWrite;
#	endif // NJ_RW_LOCK_STORE_WRITE_LOCKER_ENABLED

#	if NJ_RW_LOCK_STORE_USERS_ENABLED
		/// \brief Called immediately entering into a method
		void UserEnter();

		/// \brief Called before leaving a method
		void UserLeave();

		/// \brief This counter stores the current threads accessing to this resource
		NjAtomic<NjUint> m_kUsers;
#	endif // NJ_RW_LOCK_STORE_USERS_ENABLED

		NJ_DISABLE_COPY(NjRWLock);
	};

#	include "NjRWLock_inline.h"

#endif // NJ_RW_LOCK_H
